// GeoClient.m
// By Charles G. Fleming, Educational Computing Services, Allegheny College.
// You may freely copy, distribute and reuse this code. 
// Allegheny College and the author disclaim any warranty of any kind, 
// expressed or implied, as to its fitness for any particular use.
// This work was partially supported by a grant from the Vira Heinz Endowment.


#import "GeoClient.h"
#import "Subprocess.h"

#import <appkit/Panel.h>
#import <appkit/Application.h>
#import <appkit/TextField.h>
#import <appkit/PopUpList.h>
#import <appkit/MenuCell.h>
#import <appkit/ScrollView.h>
#import <appkit/Text.h>
#import <appkit/Button.h>

#import <objc/Storage.h>
#import <objc/Object.h>

#import <strings.h>
#import <string.h>
#import <stdlib.h>

@implementation GeoClient

int stuffer(struct dataRecord* , char *);
    
- appDidInit:sender
{      
    subprocess = [[Subprocess allocFromZone:[self zone]] init:"/bin/csh"  withDelegate:self 										andPtySupport:YES andStdErr:YES]; 
    [subprocess send:"telnet martini.eecs.umich.edu 3000"];

    records = [[Storage allocFromZone:[self zone]] initCount:10 
         elementSize:sizeof(struct dataRecord)
         description:"{[50c][50c][50c][50c][50c][50c][50c][50c][50c][50c][50c][50c][50c][50c][50c][50c][50c]}"];

    // Set up the pull down lists.
    statesPullDownList = [statesPullDownListButton target];
    [statesPullDownList setTarget:self];
    [statesPullDownList setAction:@selector(selectState:)];  
    moreStatesPullDownList = [moreStatesPullDownListButton target];
    [moreStatesPullDownList setTarget:self];
    [moreStatesPullDownList setAction:@selector(selectState:)];  

    // Display a message until login is complete.
    [NXApp runModalFor:loginMessageWindow];	  
    return self;
}

// Query the database for information on a particular city.    
- query:sender
{
    char *cityAndState;

    // Enable the button only after the query is finished.
    [sender setEnabled:NO];    	   
    
    // So that we will receive text did change messages from our text fields.
   [self makeFirstResponder:self];

    // Reset the pull down list, clear the data records in the storage object and clear the
    // text fields.
    [self resetPullDownList];
    [records empty];
    [self clearTextFields];
    
    cityAndState = malloc(strlen([cityTextField stringValue])+strlen([stateTextField stringValue])+3);
    
    // Build the query string for the database server.  The query string will be  "City, State"  or "City".
    if(strlen([stateTextField stringValue]) > 0)
	sprintf(cityAndState,  "%s, %s", [cityTextField stringValue],[stateTextField stringValue]);
    else
        strcpy(cityAndState, [cityTextField stringValue]);
    
    // Send the query to the subprocess object.	   
    [subprocess send:cityAndState];

    // Searching message window.
    [searchingMessageWindow makeKeyAndOrderFront:self];
    free(cityAndState);	  
    return self;
}

// Put the information for the state selected from the pull down list into the text fields.
- selectState:sender
{
    int tag;
    struct dataRecord *newRecord;
 
    // Get the record for the selected state.   
    tag = [[sender selectedCell] tag];
    newRecord = (struct dataRecord *)[records elementAt:tag];

     // Put the information into the text fields.
    [stateTextField setStringValue:newRecord->stateName];    
    [countyCodeTextField setStringValue:newRecord->countyCode];
    [countyNameTextField setStringValue:newRecord->countyName];
    [stateAbbrTextField setStringValue:newRecord->stateAbbr];
    [stateNameTextField setStringValue:newRecord->stateName];
    [nationAbbrTextField setStringValue:newRecord->nationAbbr];
    [nationNameTextField setStringValue:newRecord->nationName];
    [areaCodeTextField setStringValue:newRecord->areaCode];
    [elevationTextField setStringValue:newRecord->elevation];
    [featureCodeTextField setStringValue:newRecord->featureCode];
    [featureNameTextField setStringValue:newRecord->featureName];
    [latitudeTextField setStringValue:newRecord->latitude];
    [longitudeNameTextField setStringValue:newRecord->longitude];
    [censusTextField setStringValue:newRecord->census];
    [remarkTextField setStringValue:newRecord->remark];
    [timeZoneTextField setStringValue:newRecord->timeZone];
    [[zipScrollView docView] setText:newRecord->zip];
    return self;
}

// Clear all of the text fields.
- clearTextFields
{
    [countyCodeTextField setStringValue:""];
    [countyNameTextField setStringValue:""];
    [stateAbbrTextField setStringValue:""];
    [stateNameTextField setStringValue:""];
    [nationAbbrTextField setStringValue:""];
    [nationNameTextField setStringValue:""];
    [areaCodeTextField setStringValue:""];
    [elevationTextField setStringValue:""];
    [featureCodeTextField setStringValue:""];
    [featureNameTextField setStringValue:""];
    [latitudeTextField setStringValue:""];
    [longitudeNameTextField setStringValue:""];
    [censusTextField setStringValue:""];
    [remarkTextField setStringValue:""];
    [timeZoneTextField setStringValue:""];
    [[zipScrollView docView] setText:""];
    return self;
}

// Remove the entries from the pull down lists.
- resetPullDownList
{
    int statesCount, state;

    statesCount = [statesPullDownList count];
    for(state = statesCount-1; state > 0; state--)
        [statesPullDownList removeItemAt:state];

    statesCount = [moreStatesPullDownList count];
    for(state = statesCount-1; state > 0; state--)
        [moreStatesPullDownList removeItemAt:state];
    return self;
}  
            
- textDidChange:textObject
{
    TextField *textField;
    
    // Clear the text fields.
    [self clearTextFields];
    
    // Reset the pull down list.
    [self resetPullDownList];    
    [statesPullDownListButton setEnabled:NO];         
    [moreStatesPullDownListButton setEnabled:NO];         
    
    // Emtpy all city records.    
    [records empty];
    
    textField = [textObject delegate];    
    if(textField == cityTextField)
        [stateTextField setStringValue:""];    
    return self;
}
    
- appWillTerminate:sender
{
    [subprocess send:"bye"];
    [subprocess terminate:self];
    return self;
}

// Process the output from the subprocess object. 
- subprocessOutput:(char *)buffer
{  
    static char *message;
    char *period, *start, *end;
    static BOOL firstTime = YES;
    static BOOL login = YES;
    int cityCount;
    MenuCell *menuCell;

    // Malloc a message buffer only once.
    if(firstTime)
    {
        message = malloc(10000);
	firstTime = NO;  
     }  

    // Process a login.       
    strcat(message, buffer);
    if(login)
    {    
        period = rindex(message, '.');
        if(period )
            if(*(period - 1) == '\n')
    	    {  
	        [loginMessageWindow close];
	        [NXApp stopModal];
		[queryButton setEnabled:YES];     
                bzero(message, strlen(message)+1);  
                *(message+1) = '\0';    
		login = NO;     
            }	
    }	
    // Process the data returned from the database server.
    else
    {
        // Process the records only after all buffers have arrived.
        if(strstr(message, "\n."))
	{  
	    // No cities found.
	    if(!strstr(message, "\n0"))
	    {
                bzero(message, strlen(message)+1);  
		[searchingMessageWindow close];
		[queryButton setEnabled:YES];     
	    }  
	    // Process the cities.
            else
            {  
                // Process all records but the last one (states).
	        cityCount = 0;
                start = strstr(message, "\n0")+1;
	        while(end = strstr(start, "\n0"))
	        {
	            *end = '\0';
	            // Clear out the record and then stuff all of the information into it.
		    bzero(&record, sizeof(struct dataRecord));
		    
		    if(stuffer(&record, start))
		    {    
		        if(cityCount < 39)
			{
	                    // Check for duplicate states.  The data base has some duplicates.
                            while( [statesPullDownList indexOfItem:record.stateName]  != -1)
	                        strcat(record.stateName, " ");

	                    // Get the state and add a button to the pulldown list.
 	                    menuCell = [statesPullDownList addItem:record.stateName];
 		            [menuCell setTag:cityCount];  
			}
			else
			{
	                    // Check for duplicate states.  The data base has some duplicates.
                            while( [moreStatesPullDownList indexOfItem:record.stateName]  != -1)
	                        strcat(record.stateName, " ");

	                    // Get the state and add a button to the pulldown list.
 	                    menuCell = [moreStatesPullDownList addItem:record.stateName];
 		            [menuCell setTag:cityCount];  
			}
	                [records addElement:&record];  
	                cityCount++;
		    }	    
	            start = end+1;
	        }
	
	        // Now process the last record (state).
	        end = strstr(start, "\n.");
	        *end = '\0';
	
	        // Clear out the record and then stuff all of the information into it.
		bzero(&record, sizeof(struct dataRecord));	        
		if(stuffer(&record, start))
		{
		    if(cityCount < 39)
		    {
	                // Check for duplicate states.  The data base has some duplicates.
                        while( [statesPullDownList indexOfItem:record.stateName]  != -1)
	                   strcat(record.stateName, " ");

	                // Get the state and add a button to the pulldown list.
 	                menuCell = [statesPullDownList addItem:record.stateName];
 		        [menuCell setTag:cityCount];  
		    }
		    else
		    {
	                // Check for duplicate states.  The data base has some duplicates.
                        while( [moreStatesPullDownList indexOfItem:record.stateName]  != -1)
	                    strcat(record.stateName, " ");

	                // Get the state and add a button to the pulldown list.
 	                menuCell = [moreStatesPullDownList addItem:record.stateName];
 		        [menuCell setTag:cityCount];  
		    }
	            [records addElement:&record];  
	            cityCount++;
		}
		if(cityCount > 0)    
		    [statesPullDownListButton setEnabled:YES];  
		else
		    [statesPullDownListButton setEnabled:NO];  
		if(cityCount > 38)
		    [moreStatesPullDownListButton setEnabled:YES];             
		else
		    [moreStatesPullDownListButton setEnabled:NO];  
                bzero(message, strlen(message)+1);  
		[searchingMessageWindow close];
                [queryButton setEnabled:YES];		
            }
        }	  
    }   	
    return self;
}
    
// This is sent when the connection is broken, or when we quit the application.
- subprocessDone
{
    NXRunAlertPanel("BYE", "You are no longer connected to the remote machine.", 
    						NULL, NULL, NULL);
    return self;
}

// Display any error messages received.
- subprocessError:(const char *)errorString
{
    [errorMessageWindow makeKeyAndOrderFront:self];
    [[errorMessageScrollView docView] setText:errorString];
    return self;
}

// Takes each string, corresponding to a city and specific state,  and puts the information 
// into a record.
int stuffer(struct dataRecord *record , char *entry)
{
    char *zero, *start, *copy, *linefeed, *string, *nextSpace; 
    BOOL stateFound = NO, done = NO;
    int firstChar, blank;

    copy = malloc(strlen(entry) + 1);
    strcpy(copy, entry);

    // Find the city field and put it into the structure.       
    zero = index(copy, '0')+2;
    linefeed = index(zero, '\n');
    *(linefeed) = '\0';
    strcpy(record->city, zero);

    // Get the fields and put them into the structure.
    start = 	linefeed +1;
    while(!done)
    {
        // Put a null terminator at the end of each line before processing the line.
        linefeed = index(start, '\n');
  	*linefeed = '\0';
	
        // Based on the first character, put the field into the structure.
        firstChar = (int)*(start);
	    
        switch(firstChar)
	{	    
	        case '1':
		    // Get the county code.
	            string = index(start, ' ')+1;
		    nextSpace = index(string, ' ');
		    *nextSpace = '\0';
                    strcpy(record->countyCode, string);

		    // Get the county name.	  
	            string = nextSpace+1;
                    strcpy(record->countyName, string);	
		    break;  			  
	        case '2':
		    stateFound = YES;
		    // Get the state abbreviation.
	            string = index(start, ' ')+1;
		    nextSpace = index(string, ' ');
		    *nextSpace = '\0';
                    strcpy(record->stateAbbr, string);
			
		    // Get the state name.	  
	            string = nextSpace+1;
                    strcpy(record->stateName, string);
                    break;  
	        case '3':
		    // Get the nation abbreviation.
	            string = index(start, ' ')+1;
		    nextSpace = index(string, ' ');
		    *nextSpace = '\0';
                    strcpy(record->nationAbbr, string);
			
		    // Get the nation name.	  
	            string = nextSpace+1;
                    strcpy(record->nationName, string);
                    break;  
	        case 'A':
		    // Get the area code.
	            string = index(start, ' ')+1;
                    strcpy(record->areaCode, string);
                    break;  
	        case 'E':
		    // Get the elevation.
	            string = index(start, ' ')+1;
                    strcpy(record->elevation, string);
                    break;  
	        case 'F':
		    // Get the feature code.
	            string = index(start, ' ')+1;
		    nextSpace = index(string, ' ');
		    *nextSpace = '\0';
                    strcpy(record->featureCode, string);
			
		    // Get the feature name.	  
	            string = nextSpace+1;
                    strcpy(record->featureName, string);
                    break;  
	        case 'L':			 	  
		    // Get the latitude.
	            string = index(start, ' ')+1;
		    nextSpace = index(string, ' ');	
		    for(blank = 0; blank < 3; blank++)
		        nextSpace = index(nextSpace+1, ' ');
		    *nextSpace = '\0';
                    strcpy(record->latitude, string);

		    // Get the longitude.	  
	            string = nextSpace+1;
                    strcpy(record->longitude, string);		
		    break;  
	        case 'P':
		    // Get the census.
	            string = index(start, ' ')+1;
                    strcpy(record->census, string);
                    break;  
	        case 'R':
		    // Get the remark.
	            string = index(start, ' ')+1;
                    strcpy(record->remark, string);
                    break;  		 
	        case 'T':
		    // Get the time zone.
	            string = index(start, ' ')+1;
                    strcpy(record->timeZone, string);
                    break;  
	        case 'Z':			  	  
		    // Get the zip codes.
	            string = index(start, ' ')+1;
                    strcat(record->zip, string);
                    break;  			  
	}

	if(index(linefeed+1, '\n'))
	    start = linefeed + 1;
	else
	    done = YES;    
    }
    free(copy); 
    return stateFound;
}    
    
@end
